package com.eischet.lookup.web;

import org.apache.log4j.Logger;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.store.FSDirectory;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Required;

import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * The SearchIndex allows searching a Lucene index.
 */
public class SearchIndex {
    private static final Logger logger = Logger.getLogger(SearchIndex.class);

    private AccessController accessController;
    // private IndexSearcher indexSearcher = null;
    private List<SearchableCategory> searchableCategories = Collections.emptyList();
    private List<SearchableType> searchableTypes = new ArrayList<SearchableType>();
    private boolean enableRecencyBoosting;
    private String path = null;
    private String remedyUrlBase;

    private static final Object indexReaderLock = new Object();
    private static final int MAX_INDEX_READER_USAGES = 50;
    private int indexReaderUsages;

    /**
     * @return the base URL of the ARS web application
     */
    public String getRemedyUrlBase() {
        return remedyUrlBase;
    }

    /**
     * Sets the base URL of the ARS web application
     * @param remedyUrlBase the base URL, e.g. http://server:port/path/
     */
    @Required
    public void setRemedyUrlBase(String remedyUrlBase) {
        this.remedyUrlBase = remedyUrlBase;
    }

    /**
     * Finds the searchable object type with the given name, e.g. "problemworklog".
     * @param name the name, as configured in the Spring context
     * @return the searchable type, or null if no type object of that name exists
     */
    public SearchableType getType(String name) {
        for (SearchableType type : searchableTypes) {
            if (type.getName().equalsIgnoreCase(name)) {
                return type;
            }
        }
        return null;
    }

    /**
     * Sets the access controller instance that will be used to determine which records can be seen by a user.
     * @param accessController an access controller instance
     */
    @Required
    public void setAccessController(AccessController accessController) {
        this.accessController = accessController;
    }

    /**
     * Configures where to look for the Lucene index, e.g. c:/lookup.
     * @param path the path to the full text search index
     */
    @Required
    public void setPath(String path) {
        this.path = path;
    }

    /**
     * @return the index path, e.g. c:/lookup
     */
    String getPath() {
        return path;
    }

    /**
     * @return the list of searchable object categories
     */
    public List<SearchableCategory> getSearchableCategories() {
        return searchableCategories;
    }

    /**
     * Set the types of objects searchable in the web application; other types might be found, but will
     * not be "known" to the application, e.g. they will not show up in the type filter drop down list.
     * @param searchableCategories all categories of index objects that should be known to the application
     */
    @Required
    public void setSearchableCategories(List<SearchableCategory> searchableCategories) {
        this.searchableCategories = searchableCategories;
        // copy over all types
        for (SearchableCategory category : searchableCategories) {
            for (SearchableType searchableType : category.getSearchableTypes()) {
                searchableTypes.add(searchableType);
                searchableType.setSearchIndex(this);
                searchableType.setCategory(category);
            }
        }
    }

    /**
     * Turns recency boosting on or off; when set to on, searches will be ordered by recency (last update time stamp), descending.
     * Recency boosting increases the amount of work that Lucene has to perform when searching the index. If you don't need
     * this feature, turn it off to increase searching speed.
     * @param enableRecencyBoosting true to enable, false to disable
     */
    @Required
    public void setEnableRecencyBoosting(boolean enableRecencyBoosting) {
        this.enableRecencyBoosting = enableRecencyBoosting;
    }

    /**
     * @return an index searcher for the Lucene index
     * @throws IndexInaccessibleError
     */
    @NotNull
    IndexSearcher getIndexSearcher() throws IndexInaccessibleError {
        try {
            File file = new File(getPath());
            FSDirectory dir = FSDirectory.open(file);
            IndexSearcher indexSearcher = new IndexSearcher(dir);
            return indexSearcher;
        } catch (Exception ex) {
            throw new IndexInaccessibleError(ex);
        }
    }

    public void close(IndexSearcher indexSearcher) {
        if (indexSearcher != null) {
            try {
                indexSearcher.close();
            } catch (IOException e) {
                logger.warn(String.format("error closing IndexSearcher %s", indexSearcher), e);
            }
        }
    }
    
    /**
     * Queries the Lucene index and returns an object that can be used to access the search results.
     *
     * @param loginName     the user's login name, used by the access controller
     * @param session       the user's web session, used by the access controller
     * @param userQuery     the user's query, as entered in the web application
     * @param filter        an optional query filter
     * @return              a query object
     * @throws Exception    for errors
     */
    public LuceneQuery query(String loginName, HttpSession session, String userQuery, Filter filter) throws Exception {
        return new LuceneQuery(this, getIndexSearcher(), userQuery,
            enableRecencyBoosting, accessController, loginName, session, filter);
    }

    /**
     * @return a string representation of the object
     */
    @Override
    public String toString() {
        return String.format("SearchIndex{enableRecencyBoosting=%s, path='%s'}", enableRecencyBoosting, path);
    }

    /**
     * Search for a document with the specified handle.
     *
     * @param handle                    document handle
     * @return                          documents that match (max. 1)
     * @throws IndexInaccessibleError   for errors
     * @throws IOException              for errors
     */
    public List<SearchResult> searchForHandle(String handle, String loginName, HttpSession session) throws IndexInaccessibleError, IOException {
        List<SearchResult> results = new ArrayList<SearchResult>(1);
        Query tq = new TermQuery(new Term("handle", handle));
        tq = accessController.rewriteQuery(loginName, session, tq);
        TopDocs hits = getIndexSearcher().search(tq, 1);
        if (hits.totalHits == 1) {
            Document doc = getIndexSearcher().doc(hits.scoreDocs[0].doc);
            SearchResult res = new SearchResult(this, doc, null);
            results = new ArrayList<SearchResult>(1);
            results.add(res);
        } else {
            logger.warn(String.format("raw view handle not found: %s, totalHits=%d", handle, hits.totalHits));
        }
        return results;
    }
}
